﻿// INGENIOUS FRAMEWORK - Semantic Web Development Simplified
// Copyright (C) 2012 Linked Data Tools

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Threading;
using System.Reflection;
using System.Linq.Expressions;

using Ingenious.Core;

namespace Ingenious.Graph
{
    /// <summary>
    /// A graph is a container of subject nodes. It always has a first node, to which
    /// other nodes may be added (as predicates).
    /// 
    /// Note: TTripleId applies to the unique ID by which the triple item is stored.
    /// It may not be a URI, as the target store may not be able to work with URIs.
    /// </summary>
    public class Graph<TTripleId>
    {
        #region Constants
        private const string DefaultBaseUri = "http://www.linkeddatatools.com/";
        #endregion

        private readonly GraphProxy<TTripleId> m_graphProxy;
        private readonly string m_name;        

        private readonly ReaderWriterLockSlim m_transactionLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

        private readonly HashSet<NamespaceUri> m_namespaceUris = new HashSet<NamespaceUri>();

        private readonly Dictionary<Uri, Node<TTripleId>> m_nodesBySubjectUri = new Dictionary<Uri, Node<TTripleId>>();

        #region Object Lifetime
        public Graph(GraphProxy<TTripleId> graphProxy, string name)
        {
            if (graphProxy == null)
            {
                throw new ArgumentNullException("modelProxy");
            }

            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
           
            m_graphProxy = graphProxy;
            m_name = name;
        }
        #endregion

        #region Node Factory Methods
        public Node<TTripleId> GetNode(Uri subjectUri, NamespaceUri namespaceUri = null)
        {
            GraphScope<TTripleId> graphScope = null;
            GraphScope<TTripleId> implicitGraphScope = null;

            // First verify that this thread has an open write transaction scope            
            // before we allow the setting of a property
            if (!Proxy.HasOpenWriteGraphScope(Thread.CurrentThread.ManagedThreadId, out graphScope))
            {
                // This will throw if the caller already has a read transaction open
                graphScope = new GraphScope<TTripleId>(this, GraphLock.Write, Proxy.ImplicitFetchMode);
                implicitGraphScope = graphScope;
            }

            // Add to the namespaces for this graph (if not already added)
            if (namespaceUri != null)
            {
                m_namespaceUris.Add(namespaceUri);
            }

            Node<TTripleId> node = null;

            // If this is a new node request, create it, otherwise retrieve the current node
            if (!m_nodesBySubjectUri.TryGetValue(subjectUri, out node))
            {
                node = new Node<TTripleId>(subjectUri, this, graphScope.FetchMode);
                m_nodesBySubjectUri[subjectUri] = node;
            }

            if (implicitGraphScope != null)
            {
                implicitGraphScope.Dispose();
                implicitGraphScope = null;
            }

            return node;
        }

        public Node<TTripleId> GetNode(string relativeSubjectUri, NamespaceUri namespaceUri)
        {
            // Form the full subject URI
            Uri subjectUri = null;
            string subjectUriString = namespaceUri.Uri.ToString() + relativeSubjectUri;
            if (!Uri.TryCreate(subjectUriString, UriKind.Absolute, out subjectUri))
            {
                throw new NodeException("The relative subject URI '" + relativeSubjectUri + "' is not a valid URI");
            }

            return GetNode(subjectUri, namespaceUri);
        }
        #endregion

        #region Graph Factory Methods
        public static Graph<TTripleId> Get(GraphProxy<TTripleId> graphProxy, string graphName)
        {
            if (graphProxy == null)
            {
                throw new ArgumentNullException("graphProxy");
            }

            if (graphName == null)
            {
                throw new ArgumentNullException("graphName");
            }

            Graph<TTripleId> graph = graphProxy.GraphStore.GetGraph(graphName);

            return graph;
        }

        public static Graph<TTripleId> Get(GraphProxy<TTripleId> graphProxy, string graphName, Uri firstNodeSubjectUri)
        {
            if (graphProxy == null)
            {
                throw new ArgumentNullException("graphProxy");
            }

            if (graphName == null)
            {
                throw new ArgumentNullException("graphName");
            }
            
            if (firstNodeSubjectUri == null)
            {
                throw new ArgumentNullException("firstNodeSubjectUri");
            }

            return graphProxy.GraphStore.GetGraph(graphName);
        }

        public static Graph<TTripleId> Get(GraphProxy<TTripleId> graphProxy, string graphName, string firstNodeSubjectUriString)
        {
            if (graphProxy == null)
            {
                throw new ArgumentNullException("graphProxy");
            }

            if (graphName == null)
            {
                throw new ArgumentNullException("graphName");
            }

            if (string.IsNullOrEmpty(firstNodeSubjectUriString))
            {
                throw new ArgumentException("firstNodeSubjectUriString");
            }

            Uri firstNodeSubjectUri = null;
            if (!Uri.TryCreate(firstNodeSubjectUriString, UriKind.Absolute, out firstNodeSubjectUri))
            {
                throw new ArgumentException("firstNodeSubjectUriString");
            }

            return graphProxy.GraphStore.GetGraph(graphName);
        }
        #endregion

        #region Serialization/Deserialization

        public List<Triple<TTripleId>> ToTriples()
        {
            List<Triple<TTripleId>> triples = new List<Triple<TTripleId>>();

            foreach (Node<TTripleId> node in m_nodesBySubjectUri.Values)
            {
                triples.AddRange(node.ToTriples());
            }

            return triples;
        }
   
        public static Graph<TTripleId> FromTriples(GraphProxy<TTripleId> graphProxy, string name, NamespaceUriCollection namespaceUriCollection, IList<Triple<TTripleId>> triples)
        {
            if (graphProxy == null)
            {
                throw new ArgumentNullException("graphProxy");
            }

            if (namespaceUriCollection == null)
            {
                throw new ArgumentNullException("namespaceUriCollection");
            }

            if (triples == null)
            {
                throw new ArgumentNullException("triples");
            }

            if (triples.Count == 0)
            {
                throw new GraphException("Cannot create an empty graph");
            }

            Graph<TTripleId> graph = Graph<TTripleId>.Get(graphProxy, name, triples.First().Subject);

            // Create a writable graph transaction scope
            using (new GraphScope<TTripleId>(graph, GraphLock.Write))
            {
                // Add the namespace URIs
                foreach (NamespaceUri namespaceUri in namespaceUriCollection)
                {
                    graph.m_namespaceUris.Add(namespaceUri);
                }

                // Order namespace URIs by length to ensure we take the most concise URI as the
                // property name for the triple
                IEnumerable<NamespaceUri> orderedNamespaceUris =
                    graph.m_namespaceUris.OrderByDescending(nu => nu.Uri.ToString().Length);

                // Create nodes grouped by subject
                IEnumerable<IGrouping<Uri, Triple<TTripleId>>> tripleGroups =
                    triples.GroupBy<Triple<TTripleId>, Uri>(t => t.Subject);

                foreach (IGrouping<Uri, Triple<TTripleId>> tripleGroup in tripleGroups)
                {
                    Node<TTripleId> node = graph.GetNode(tripleGroup.Key);
                    
                    foreach (Triple<TTripleId> triple in tripleGroup)
                    {
                        string predicateUriString = triple.Predicate.ToString();

                        if (triple.IsLiteral)
                        {
                            node.SetPropertyValue(predicateUriString, triple.LiteralType, triple.Value);                            
                        }
                        else
                        {
                            // It's not a literal. We need to create (or get) a node with
                            // a subject URI equal to this object URI
                            node.SetPropertyValue(predicateUriString, typeof(INode<TTripleId>), graph.GetNode(triple.Object));
                        }
                    }
                }
            }

            return graph;
        }
        #endregion

        #region Properties
        /// <summary>
        /// The proxy handling data updates on the graph
        /// </summary>
        public GraphProxy<TTripleId> Proxy
        {
            get { return m_graphProxy; }
        }

        /// <summary>
        /// The name of the graph
        /// </summary>
        public string Name
        {
            get { return m_name; }
        }

        /// <summary>
        /// The reader/writer lock for the graph (use for transaction management)
        /// </summary>
        public ReaderWriterLockSlim Lock
        {
            get { return m_transactionLock; }
        }

        /// <summary>
        /// The namespace URIs used by the predicates (property names) within the graph
        /// </summary>
        public HashSet<NamespaceUri> NamespaceUris
        {
            get { return m_namespaceUris; }
        }

        public Dictionary<Uri, Node<TTripleId>> GraphNodes
        {
            get { return m_nodesBySubjectUri; }
        }
        #endregion
    }
}
